package com.nts.visionworks.core.ffmpeg;

import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import javax.servlet.http.HttpSession;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.nts.visionworks.core.concurrent.ProcessCallable;

/**
 * FFMpeg을 이용해 Frame 이미지(섬네일)를 저장하기 위한 인터페이스를 제공한다.
 * @author Devcken
 */
public class FFMpegFrameExtractor
{
	private static final Logger logger = LoggerFactory.getLogger(FFMpegFrameExtractor.class);
	
	/**
	 * 임시 파일의 확장자 상수를 나타낸다.
	 */
	private static final String _TEMPORARY_EXTENSION = "thumb";
	
	/**
	 * Frame 이미지의 확장자 상수를 나타낸다.
	 */
	public static final String _FRAME_IMAGE_EXTENSION = ".jpg";
	
	/**
	 * FFMpeg 실행 파일의 경로를 나타낸다.
	 */
	private String _ffmpegPath;
	
	/**
	 * FFMpeg 실행을 위한 스레드에 대한 대기 시간을 설정한다. 스레드가 대기 시간을 초과하여 실행되는 경우 타임아웃으로 처리된다.
	 */
	private long _timeoutMilli = 60000;
	
	/**
	 * FFMpeg을 이용해 Frame 이미지를 추출한다.
	 * @param sourcePath Frame 이미지를 추출하고자 하는 영상 파일의 전체 경로를 설정한다.
	 * @param outputPath 추출하려는 Frame 이미지 파일의 전체 경로를 설정한다.
	 * @param sourceWidth 영상 파일의 너비를 설정한다.
	 * @param sourceHeight 영상 파일의 높이를 설정한다.
	 * @param maxWidth 추출하고자 하는 Frame 이미지의 최대 너비를 설정한다.
	 * @param timeSecond Frame 이미지를 추출하고자 하는 영상의 시간(초)을 설정한다.
	 * @param session Request에 대한 사용자를 구분하고 그에 대한 정보를 저장하는 방법을 제공한다. {@link javax.servlet.ServletContext} 객체를 가져오기 위해 필요로 한다.
	 * @return Frame 이미지가 정상적으로 추출된 경우 true, 그렇지 않으면 false가 전달된다.
	 */
	public boolean extractFrame(String sourcePath, String outputPath, int sourceWidth, int sourceHeight,
			int maxWidth, int timeSecond, HttpSession session)
	{
		try
		{
			// Frame 이미지에 대한 임시 파일 경로를 설정한다.
			String tempFilePath = sourcePath.replace(FilenameUtils.getExtension(sourcePath), "") + _TEMPORARY_EXTENSION;
			
			// Frame 이미지 파일의 확장자를 설정한다.
			outputPath = outputPath.replace("." + FilenameUtils.getExtension(outputPath), "") + _FRAME_IMAGE_EXTENSION;
			
			// 변환 사이즈를 구성한다.([width]x[height])
			String size = maxWidth + "x" + (maxWidth * sourceHeight / sourceWidth);
			
			// FFMpeg 파라메터를 구성한다.
			String[] parameters = new String[]
					{
						"cmd.exe",
						"/C",
						"start",
						"/wait",
						session.getServletContext().getRealPath("/") + this.getFfmpegPath(),
						String.valueOf(timeSecond),
						String.format("\"%s\"", sourcePath),
						size,
						String.format("\"%s\"", tempFilePath)
					};
			
			// 스레드를 시작한다. 
			this.startThread(parameters);
			
			File tempFile = new File(tempFilePath);
			
			// FFMpeg으로부터 Frame 이미지(임시)가 생성되면 파일의 확장자를 변경한다.
			if (tempFile.exists())
			{
				// 생성된 임시 파일의 크기가 0바이트인 경우, 실패로 간주한다.
				if (tempFile.length() == 0)
				{
					FileUtils.deleteQuietly(tempFile);
					
					return false;
				}
				
				try
				{
					File outputFile = new File(outputPath);
					
					// Frame 이미지 파일이 이미 존재할 경우 삭제한다.
					if (outputFile.exists())
					{
						FileUtils.deleteQuietly(outputFile);
					}
					
					// 임시 파일을 Frame 이미지 파일로 변경한다.
					FileUtils.moveFile(tempFile, outputFile);
					
					return true;
				}
				catch (IOException error)
				{
					logger.error("임시 섬네일 파일을 복사하는 도중에 오류가 발생했습니다.", error);
				}
			}
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
		}
		
		return false;
	}
	
	/**
	 * FFMpeg 실행을 위한 스레드를 시작한다.
	 * @param parameters FFMpeg 실행을 위한 파라메터를 설정한다.
	 * @param timeoutMilli 스레드에 대한 대기 시간을 설정한다. 스레드가 대기 시간을 초과하여 실행되는 경우 타임아웃으로 처리된다. 
	 * @return 주어진 대기 시간 내에 실행이 완료되면 true, 그렇지 않으면 false를 반환한다.
	 * @throws TimeoutException FFMpeg 실행 스레드가 타임아웃될 때 발생하는 예외로 발생하면 실패로 간주한다.
	 */
	private boolean startThread(final String[] parameters)
	{
		ExecutorService executor = Executors.newSingleThreadExecutor();
		
		Set<ProcessCallable<Void>> callables = new HashSet<ProcessCallable<Void>>();
		
		callables.add(new ProcessCallable<Void>(parameters));
		
		try
		{
			List<Future<Void>> futures = executor.invokeAll(callables, this._timeoutMilli, TimeUnit.MILLISECONDS);
			
			if (!futures.get(0).isDone())
			{
				for (ProcessCallable<Void> callable : callables)
				{
					callable.shutdownProcess();
				}
			}
			
			executor.shutdown();
		}
		catch (InterruptedException error)
		{
			logger.error(error.getMessage(), error);
		}
		
		return true;
	}

	public String getFfmpegPath()
	{
		return this._ffmpegPath;
	}

	public void setFfmpegPath(String ffmpegPath)
	{
		this._ffmpegPath = ffmpegPath;
	}

	public long getTimeoutMilli()
	{
		return this._timeoutMilli;
	}

	public void setTimeoutMilli(long timeoutMilli)
	{
		this._timeoutMilli = timeoutMilli;
	}
}